Sources/XCMetricsClient/Hardware Management/HardwareFactsFetcher.swift (168 lines of code) (raw):

// Copyright (c) 2020 Spotify AB. // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import Foundation import Darwin enum HardwareFactsFetcherError: LocalizedError { case systemInfoError(String) var localizedDescription: String { switch self { case .systemInfoError(let systemInfoPath): return "\(systemInfoPath) file not found in host." } } } /// Enum with the name of some `sysctl` property names enum SysctlProperty: String { case cpuModel = "machdep.cpu.brand_string" case cpuCount = "hw.ncpu" case cpuFrequencyMax = "hw.cpufrequency_max" case hardwareModel = "hw.model" case hostOSFamily = "kern.ostype" case memoryTotal = "hw.memsize" case memoryPageSize = "vm.pagesize" case memoryPageFreeCount = "vm.page_free_count" case memoryPageSpeculativeCount = "vm.page_speculative_count" case swapUsage = "vm.swapusage" case uptime = "kern.boottime" case osVersion = "kern.osproductversion" case sleepTime = "kern.sleeptime" } protocol HardwareFactsFetcher { /// Fetches the Hardware facts of the current host /// - Returns: An instance of `HardwareFacts` /// - Throws: An `HardwareFactsFetcherError` if there was an error fetching the facts func fetch() throws -> BuildHost } /// Gets the Hardware facts for the host, it usually relies on `sysctlbyname` to get them class HardwareFactsFetcherImplementation: HardwareFactsFetcher { private static let systemVersionPath = "/System/Library/CoreServices/SystemVersion.plist" func fetch() throws -> BuildHost { var facts = BuildHost() facts.cpuCount = Int32(self.cpuCount) facts.cpuModel = self.cpuModel facts.cpuSpeedGhz = self.cpuSpeedGhz facts.hostArchitecture = self.hostArchitecture facts.hostModel = self.hostModel facts.hostOs = try self.getHostOS() facts.hostOsFamily = self.hostOSFamily facts.hostOsVersion = self.hostOSVersion facts.isVirtual = self.isVirtual facts.memoryFreeMb = self.memoryFreeMb facts.memoryTotalMb = self.memoryTotalMb facts.swapFreeMb = self.swapFreeMb facts.swapTotalMb = self.swapTotalMb facts.timezone = self.timeZone facts.uptimeSeconds = Int64(self.uptimeSeconds) return facts } /// Value read from `SystemVersion.plist`. Could be either `Mac OS X` or `Mac OS X Server` func getHostOS() throws -> String { let systemVersionPath = HardwareFactsFetcherImplementation.systemVersionPath guard let systemInfo = NSDictionary(contentsOfFile: systemVersionPath), let productName = systemInfo["ProductName"] as? String else { throw HardwareFactsFetcherError.systemInfoError(systemVersionPath) } return productName } /// Model of the CPU as returned by the `machdep.cpu.brand_string` sysctl property lazy var cpuModel: String = { return getPropertyStringValue(.cpuModel) }() /// Number of CPUs as returned by the `hw.ncpu` sysctl property lazy var cpuCount: UInt32 = { return getPropertyUInt32Value(.cpuCount) }() /// The architecture used by the host as returned by `utsname.machine` lazy var hostArchitecture: String = { var utsName = utsname() uname(&utsName) let machineMirror = Mirror(reflecting: utsName.machine) let machine = machineMirror.children.compactMap { child -> String? in guard let value = child.value as? Int8, value != 0 else { return nil } return String(UnicodeScalar(UInt8(value))) }.joined() return machine }() /// The model of the Host as returned by sysctl's `hw.model` property lazy var hostModel: String = { return getPropertyStringValue(.hardwareModel) }() /// The family of the Host (Darwin) as returned by sysctl's `kern.ostype` property lazy var hostOSFamily: String = { return getPropertyStringValue(.hostOSFamily) }() /// The CPU's frequency in Ghz as returned by sysctl's `hw.cpufrequency_max` property lazy var cpuSpeedGhz: Float = { let speedHertz = getPropertyUInt32Value(.cpuFrequencyMax) return Float(Int64(speedHertz)) / 1_000_000_000 }() lazy var hostOSVersion: String = { return getPropertyStringValue(.osVersion) }() /// The amount of memory in MB the host has, as reported by sysctl's `hw.memsize` property lazy var memoryTotalMb: Double = { let memoryTotal = getPropertyUInt64Value(.memoryTotal) return Double(Int64(memoryTotal)).bytesToMB() }() /// The amount of free memory in MB that the host has. /// uses the same formula /// [that Facter uses](https://github.com/puppetlabs/facter/blob/main/lib/src/facts/osx/memory_resolver.cc) lazy var memoryFreeMb: Double = { let hostInfo = vm_statistics64_t.allocate(capacity: 1) var size = hostVMInfo64Count _ = hostInfo.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &size) } let data = hostInfo.move() hostInfo.deallocate() let pageSize = Int64(getPropertyUInt32Value(.memoryPageSize)) let freePages = Int64(data.free_count) let speculativePages = Int64(data.speculative_count) return Double((freePages - speculativePages) * pageSize).bytesToMB() }() /// The amount of Swap memory in MB, as reported by sysctl's `vm.swapusage` property lazy var swapTotalMb: Double = { var size = getPropertySize(.swapUsage) var xswUsage = xsw_usage() sysctlbyname(SysctlProperty.swapUsage.rawValue, &xswUsage, &size, nil, 0) return Double(Int64(xswUsage.xsu_total)) / 1_048_576 }() /// The amount of Swap memory in MB, as reported by sysctl's `vm.swapusage` property lazy var swapFreeMb: Double = { var size = getPropertySize(.swapUsage) var xswUsage = xsw_usage() sysctlbyname(SysctlProperty.swapUsage.rawValue, &xswUsage, &size, nil, 0) return Double(Int64(xswUsage.xsu_total - xswUsage.xsu_used)).bytesToMB() }() /// The uptime in seconds as reported by sysctl's `kern.boottime` property lazy var uptimeSeconds: Int = { var size = getPropertySize(.uptime) var bootTime = timeval() sysctlbyname(SysctlProperty.uptime.rawValue, &bootTime, &size, nil, 0) return bootTime.tv_sec }() /// The last time the device went to sleep as reported by sysctl's `kern.sleeptime` property. lazy var sleepTime: Int = { var size = getPropertySize(.sleepTime) var sleepTime = timeval() sysctlbyname(SysctlProperty.sleepTime.rawValue, &sleepTime, &size, nil, 0) return sleepTime.tv_sec }() /// The Host's time zone. It will return an abreviation when possible (like CEST), if not, it will /// failback to use the whole time zone identifier lazy var timeZone: String = { return TimeZone.current.abbreviation() ?? TimeZone.current.identifier }() /// True if the host is a VMware instance, false otherwise. lazy var isVirtual: Bool = { if hostModel.starts(with: "VMware") { return true } //TODO missing support for VirtualBox and Parallels return false }() private let hostVMInfo64Count = UInt32(MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size) /// Get a `sysctl` property as a String /// - parameter property: The property to get /// - returns: A String with the value of the property. /// This don't check that the property type is actually a `String`. Please check the sysctl help to know it. private func getPropertyStringValue(_ property: SysctlProperty) -> String { var size = getPropertySize(property) var value = [CChar](repeating: 0, count: size) sysctlbyname(property.rawValue, &value, &size, nil, 0) return String(cString: value) } /// Get a `sysctl` property as a UInt32 /// - parameter property: The property to get /// - returns: A String with the value of the property. /// This don't check that the property type is actually a `UInt32`. Please check the sysctl help to know it. /// If the property is not a valid UInt32, this function returns 0 private func getPropertyUInt32Value(_ property: SysctlProperty) -> UInt32 { var size = getPropertySize(property) var value: UInt32 = 0 sysctlbyname(property.rawValue, &value, &size, nil, 0) return value } /// Get a `sysctl` property as a UInt64 /// - parameter property: The property to get /// - returns: A String with the value of the property. /// This don't check that the property type is actually a `UInt64`. Please check the sysctl help to know it. /// If the property is not a valid UInt32, this function returns 0 private func getPropertyUInt64Value(_ property: SysctlProperty) -> UInt64 { var size = getPropertySize(property) var value: UInt64 = 0 sysctlbyname(property.rawValue, &value, &size, nil, 0) return value } /// Get the size of a `sysctl` property /// - parameter property: A valid `SysctlProperty` /// - returns: An `Int` with the size in bytes of the property value private func getPropertySize(_ property: SysctlProperty) -> Int { var size: Int = 0 sysctlbyname(property.rawValue, nil, &size, nil, 0) return size } }